package de.lmu.ifi.pst.uwejsf.framework;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

import javax.faces.application.NavigationHandler;
import javax.faces.context.FacesContext;

import de.lmu.ifi.pst.uwejsf.framework.process.MainProcessClassBean;
import de.lmu.ifi.pst.uwejsf.framework.process.ProcessActivityBean;
import de.lmu.ifi.pst.uwejsf.framework.process.ProcessClassBean;
import de.lmu.ifi.pst.uwejsf.framework.process.UserAction;

public abstract class AbstractUWENavigator {

	protected String currentViewID;
	protected long evaluationToken = 0;

	protected ProcessClassBean submitOrder = null;

	protected String currentTargetView = null;
	protected String currentSourceView = null;
	protected List<String> currentLinks = new ArrayList<String>();
	protected List<String> currentTargetBeans = new ArrayList<String>();
	protected String currentContentInLink = null;

	protected Map<String, Boolean> nodeActive = new StateFlagMap();

	public String getCurrentViewID() {
		return currentViewID;
	}

	public ViewInfo getCurrentView() {
		return getViewInfoMap().get(currentViewID);
	}

	/*
	 * public void setCurrentViewID(String currentViewID) { this.currentViewID =
	 * currentViewID; }
	 */
	public static AbstractUWENavigator getNavigator() {
		return (AbstractUWENavigator) ExpressionHelpers
				.resolveValue("#{uweNavigator}");
	}

	public String handleSubmit(String activityName, String actionName) {
		submitOrder = null;
		ProcessActivityBean activityBean = (ProcessActivityBean) ExpressionHelpers
				.resolveValue("#{activity" + activityName + "}");
		return activityBean.handleSubmit(actionName);
	}

	protected List<ViewInfo> getViewCandidates(String navNodeClassName) {
		ArrayList<ViewInfo> result = new ArrayList<ViewInfo>();
		for (String viewId : getViewInfoMap().keySet()) {
			ViewInfo viewInfo = getViewInfoMap().get(viewId);
			if (Arrays.asList(viewInfo.getNavigationNodes()).contains(
					navNodeClassName))
				result.add(viewInfo);
		}
		return result;
	}

	public String chooseTargetView(UserAction userAction) {

		return chooseTargetView(userAction.getProcessClassBean().getClass()
				.getName());
	}

	public String chooseTargetView(String navNodeClassName) {
		submitOrder = null;
		List<ViewInfo> candidates = getViewCandidates(navNodeClassName);
		String targetView = null;
		if (candidates.isEmpty()) {
			targetView = null;
		} else if (candidates.size() == 1) {
			targetView = candidates.iterator().next().getViewID();
		} else {
			List<ViewInfo> closestViews = getClosestToCurrentView(candidates);
			if (closestViews.isEmpty()) {
				// this would be very weird though
				targetView = null;
			}
			if (closestViews.size() == 1) {
				targetView = closestViews.iterator().next().getViewID();
			} else {
				List<ViewInfo> mostDefaultViews = getMostDefaultViews(closestViews);
				if (mostDefaultViews.size() != 1)
					throw new NavigationException(
							"Target View could not be determined! current view ID: "
									+ currentViewID
									+ " target NavigationNode: "
									+ navNodeClassName);
				targetView = mostDefaultViews.iterator().next().getViewID();
			}

		}
		return targetView;
	}

	protected List<ViewInfo> getClosestToCurrentView(List<ViewInfo> candidates) {
		ViewInfo currentViewInfo = getViewInfoMap().get(currentViewID);
		int maxMatchCount = -1;
		ArrayList<ViewInfo> closestViews = new ArrayList<ViewInfo>();
		for (ViewInfo viewInfo : candidates) {
			int matchCount = getMatchCount(currentViewInfo, viewInfo);
			if (matchCount == maxMatchCount) {
				closestViews.add(viewInfo);
			} else if (matchCount > maxMatchCount) {
				maxMatchCount = matchCount;
				closestViews.clear();
				closestViews.add(viewInfo);
			}
		}
		return closestViews;
	}

	protected int getMatchCount(ViewInfo vi1, ViewInfo vi2) {
		int matchCount = 0;
		for (PGroupInfo pGroupInfo : vi1.getPresentationGroupInfos()) {
			if (containsPGroup(vi2, pGroupInfo.getPresentationGroupName()))
				matchCount++;
		}
		return matchCount;

	}

	protected boolean containsPGroup(ViewInfo vi, String pgroupName) {
		for (PGroupInfo pGroupInfo : vi.getPresentationGroupInfos()) {
			if (pGroupInfo.getPresentationGroupName().equals(pgroupName))
				return true;
		}
		return false;
	}

	protected int getNonDefaultPGroupCount(ViewInfo vi) {
		int nonDefaults = 0;
		for (PGroupInfo pGroupInfo : vi.getPresentationGroupInfos()) {
			if (!pGroupInfo.isDefaultPGroup())
				nonDefaults++;
		}
		return nonDefaults;
	}

	protected List<ViewInfo> getMostDefaultViews(List<ViewInfo> candidates) {
		ArrayList<ViewInfo> mostDefault = new ArrayList<ViewInfo>();
		int minNonDefaultCount = Integer.MAX_VALUE;
		for (ViewInfo viewInfo : candidates) {
			int nonDefautCount = getNonDefaultPGroupCount(viewInfo);
			if (nonDefautCount == minNonDefaultCount) {
				mostDefault.add(viewInfo);
			} else if (nonDefautCount < minNonDefaultCount) {
				mostDefault.clear();
				mostDefault.add(viewInfo);
				minNonDefaultCount = nonDefautCount;
			}
		}
		return mostDefault;
	}

	public String forward() {
		// cancel registered submit actions! this is quite important!
		submitOrder = null;

		java.util.List<String> links = currentLinks;

		String targetView = null;
		for (String link : links) {
			// we assume that there will be only one process link for the forward action!
			targetView = handleInLink(link);
		}
		if (targetView == null)
			targetView = handleTargetBeans(currentTargetBeans);
		if (targetView == null)
			targetView = currentTargetView;
		return "to_" + targetView;
	}

	protected String handleInLink(String inLink) {
		String[] parts = inLink.split("\\.");
		String beanName = UWEHelpers.firstToLower(parts[0]);
		String linkName = parts[1];
		return handleInLink(beanName, linkName);
	}

	protected String handleTargetBeans(List<String> targetBeans) {
		MainProcessClassBean mainProcessClassBean = null;
		for (String beanName : targetBeans) {
			NavigationNodeBean nodeBean = (NavigationNodeBean) ExpressionHelpers
					.resolveValue("#{" + UWEHelpers.firstToLower(beanName)
							+ "}");
			if (nodeBean.checkCurrentGuards() == false)
				throw new GuardConditionViolatedException("Node guard for "
						+ beanName + " violated.");
			
			if (nodeBean instanceof MainProcessClassBean) {
				if (mainProcessClassBean != null)
					throw new NavigationException(
							"Anchor points to more than one ProcessClass!");
				mainProcessClassBean = (MainProcessClassBean) nodeBean;
			}
		}
		if (mainProcessClassBean == null)
			return null;
		return mainProcessClassBean.getActivityBean().start(getCurrentView());

	}

	protected String handleInLink(String beanName, String linkName) {
		AbstractUWEContextFactory contextProvider = AbstractUWEContextFactory
				.getCurrentInstance();
		String contentInLink = currentContentInLink;

		NavigationNodeBean nodeBean = (NavigationNodeBean) ExpressionHelpers
				.resolveValue("#{" + beanName + "}");

		// handle link's guard before setting it as content selector
		GuardHandler guardHandler = nodeBean.getGuards().get(linkName);
		if (guardHandler != null) {
			if (!guardHandler.checkCondition(contextProvider))
				throw new GuardConditionViolatedException("Condition of guard "
						+ linkName + " violated!");
		}
		// also check node guard
		if (nodeBean.getNodeGuard() != null) {
			if (!nodeBean.getNodeGuard().checkCondition(contextProvider))
				throw new GuardConditionViolatedException("Condition of "
						+ "node guard for " + beanName + " violated!");
		}

		ContentSelector cs = nodeBean.getContentSelectors().get(linkName);
		if (cs != null) {
			cs.setSourceContentInLink(contentInLink);
			nodeBean.setActiveContentSelector(cs);
		}
		String targetView = null;
		if (nodeBean instanceof MainProcessClassBean) {
			MainProcessClassBean mainProcessClassBean = (MainProcessClassBean) nodeBean;
			targetView = mainProcessClassBean.getActivityBean().start(
					getCurrentView());
		}
		return targetView;

	}

	public String handleOutgoingNavigation(MainProcessClassBean bean) {
		submitOrder = null;
		String targetView = null;
		String targetNodeName = null;
		if (bean.getNavigationRules().isEmpty()) {
			targetView = bean.getActivityBean().getActiveViewBeforeStart()
					.getViewID();
			ViewInfo viewInfo = getViewInfoMap().get(targetView);
			if (checkNodeGuardsForView(viewInfo) == false) {
				// FALLBACK
				targetView = getHomeViewID();
			}

		}

		for (NavigationRule rule : bean.getNavigationRules()) {
			if (rule.checkCondition()) {
				targetNodeName = rule.getTargetBean();
				targetView = handleInLink(rule.getTargetBean(), rule.getLink());
			}

		}
		if (targetView == null) {
			if (targetNodeName == null)
				return null;
			NavigationNodeBean navNodeBean = (NavigationNodeBean) ExpressionHelpers
					.resolveValue("#{"
							+ UWEHelpers.firstToLower(targetNodeName) + "}");
			targetView = chooseTargetView(navNodeBean.getClass().getName());
		}

		return targetView;
	}

	/**
	 * 
	 * @return
	 */
	public boolean checkNodeGuardsForView(ViewInfo viewInfo) {
		boolean check = true;

		for (int i = 0; i < viewInfo.getNavigationNodes().length
				&& check == true; i++) {

			NavigationNodeBean bean = (NavigationNodeBean) getBeanFromClassName(viewInfo
					.getNavigationNodes()[i]);
			check = bean.checkCurrentGuards();

		}
		return check;
	}

	/**
	 * - start activities - TODO: check guard conditions
	 */
	public void handleView() {
		ViewInfo viewInfo = getViewInfoMap().get(currentViewID);
		if (checkNodeGuardsForView(viewInfo) == false)
			throw new GuardConditionViolatedException(
					"Condition of active guard violated for view "
							+ currentViewID);
		nodeActive.clear();
		for (String navNodeClassName : viewInfo.getNavigationNodes()) {
			nodeActive.put(getNavigationNodeName(navNodeClassName), true);
			Object bean = getBeanFromClassName(navNodeClassName);
			if (bean instanceof ProcessClassBean) {
				ProcessClassBean processClassBean = (ProcessClassBean) bean;
				ProcessActivityBean activityBean = processClassBean
						.getActivityBean();
				if (activityBean.getActiveNode() == null)
					activityBean.runToUserAction(processClassBean
							.getUserActionName(), getCurrentView());
			}
		}
	}

	private String getNavigationNodeName(String className) {
		String[] parts = className.split("\\.");
		return parts[parts.length - 1];
	}

	private Object getBeanFromClassName(String className) {
		String[] parts = className.split("\\.");
		String beanName = UWEHelpers.firstToLower(parts[parts.length - 1]);
		return ExpressionHelpers.resolveValue("#{" + beanName + "}");
	}

	public abstract Map<String, ViewInfo> getViewInfoMap();

	public abstract String getHomeViewID();

	public AbstractUWENavigator() {

	}

	/**
	 * See {@link OGNLContentSelector}
	 * 
	 * @return
	 */
	public long getEvaluationToken() {
		return evaluationToken;
	}

	/**
	 * See {@link OGNLContentSelector}
	 * 
	 * @return
	 */
	public void setEvaluationToken(long evaluationToken) {
		this.evaluationToken = evaluationToken;
	}

	public void incEvaluationToken() {
		if (evaluationToken == Long.MAX_VALUE)
			evaluationToken = 0; // this will probably never happen :-)
		else
			evaluationToken++;
	}

	public void setCurrentViewID(String currentViewID) {
		this.currentViewID = currentViewID;
	}

	/**
	 * Handles a submit fired by a change event. This is called by {@link UWEPhaseListener}
	 * @see ProcessClassBean#handleSubmitOnChange(javax.faces.event.ValueChangeEvent)
	 */
	public void handleRegisteredSubmitOrder() {
		if (submitOrder == null)
			return;

		ProcessClassBean pcb = submitOrder;
		// set to null even if handleSubmit fails
		submitOrder = null;

		String newViewID = handleSubmit(pcb.getActivityName(), pcb
				.getUserActionName());

		ViewInfo viewInfo = getViewInfoMap().get(newViewID);
		// FALLBACK
		if (checkNodeGuardsForView(viewInfo) == false)
			newViewID = getHomeViewID();
		NavigationHandler nh = FacesContext.getCurrentInstance()
				.getApplication().getNavigationHandler();

		nh.handleNavigation(FacesContext.getCurrentInstance(), "", "to_"
				+ newViewID);
	}

	public void registerSubmitOrder(ProcessClassBean bean) {
		submitOrder = bean;
	}

	public void unRegisterSubmitOrder() {
		submitOrder = null;
	}

	public ProcessClassBean getSubmitOrder() {
		return submitOrder;
	}

	public void setSubmitOrder(ProcessClassBean submitOrder) {
		this.submitOrder = submitOrder;
	}

	public String getCurrentTargetView() {
		return currentTargetView;
	}

	public void setCurrentTargetView(String currentTargetView) {
		this.currentTargetView = currentTargetView;
	}

	public String getCurrentSourceView() {
		return currentSourceView;
	}

	public void setCurrentSourceView(String currentSourceView) {
		this.currentSourceView = currentSourceView;
	}

	public List<String> getCurrentLinks() {
		return currentLinks;
	}

	public void setCurrentLinks(List<String> currentLinks) {
		this.currentLinks = currentLinks;
	}

	public List<String> getCurrentTargetBeans() {
		return currentTargetBeans;
	}

	public void setCurrentTargetBeans(List<String> currentTargetBeans) {
		this.currentTargetBeans = currentTargetBeans;
	}

	public String getCurrentContentInLink() {
		return currentContentInLink;
	}

	public void setCurrentContentInLink(String currentContentInLink) {
		this.currentContentInLink = currentContentInLink;
	}

	public Map<String, Boolean> getNodeActive() {
		return nodeActive;
	}

	public void setNodeActive(Map<String, Boolean> nodeActive) {
		this.nodeActive = nodeActive;
	}

}